using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using Microsoft.Win32.SafeHandles;

namespace Gko.Utils
{
	/// <summary>
	/// File mapping object
	/// </summary>
	[CLSCompliant(false)]
	public class FileMapping : DisposableBase
	{
		private readonly SafeFileHandle handle;
		private readonly uint fileSize;
		private readonly List<FileMappingView> views = new List<FileMappingView>();

		/// <exception cref="ArgumentNullException"><c>handle</c> is null.</exception>
		private FileMapping(SafeFileHandle handle, uint fileSize)
		{
			if (handle == null) {
				throw new ArgumentNullException("handle");
			}
			this.handle = handle;
			this.fileSize = fileSize;
			this.AddUnmanagedDisposed(this.handle);
		}

		/// <summary>
		/// Maps a view of a file mapping into the address space of a calling process.
		/// </summary>
		/// <param name="access">The type of access to a file mapping object, which ensures the protection of the pages.</param>
		/// <returns>Mapped view.</returns>
		/// <exception cref="System.ObjectDisposedException">The instance is disposed.</exception>
		public FileMappingView MapView(FileMappingAccess access)
		{
			return this.MapView(access, this.fileSize);
		}

		/// <summary>
		/// Maps a view of a file mapping into the address space of a calling process.
		/// </summary>
		/// <param name="access">The type of access to a file mapping object, which ensures the protection of the pages.</param>
		/// <param name="size">The number of bytes of a file mapping to map to the view. All bytes must be within the maximum size specified by FileMapping.Create(). If this parameter is 0 (zero), the mapping extends from the specified offset to the end of the file mapping.</param>
		/// <returns>Mapped view.</returns>
		/// <exception cref="System.ObjectDisposedException">The instance is disposed.</exception>
		public FileMappingView MapView(FileMappingAccess access, uint size)
		{
			return this.MapView(access, 0ul, size);
		}

		/// <summary>
		/// Maps a view of a file mapping into the address space of a calling process.
		/// </summary>
		/// <param name="access">The type of access to a file mapping object, which ensures the protection of the pages.</param>
		/// <param name="offset">The file offset where the view is to begin. The offset must specify an offset within the file mapping. It is must also match the memory allocation granularity of the system. That is, the offset must be a multiple of the allocation granularity. To obtain the memory allocation granularity of the system, use the GetSystemInfo function, which fills in the members of a SYSTEM_INFO structure.</param>
		/// <returns>Mapped view.</returns>
		/// <exception cref="System.ObjectDisposedException">The instance is disposed.</exception>
		public FileMappingView MapView(FileMappingAccess access, ulong offset)
		{
			return this.MapView(access, offset, this.fileSize);
		}

		/// <summary>
		/// Maps a view of a file mapping into the address space of a calling process.
		/// </summary>
		/// <param name="access">The type of access to a file mapping object, which ensures the protection of the pages.</param>
		/// <param name="offset">The file offset where the view is to begin. The offset must specify an offset within the file mapping. It is must also match the memory allocation granularity of the system. That is, the offset must be a multiple of the allocation granularity. To obtain the memory allocation granularity of the system, use the GetSystemInfo function, which fills in the members of a SYSTEM_INFO structure.</param>
		/// <param name="size">The number of bytes of a file mapping to map to the view. All bytes must be within the maximum size specified by FileMapping.Create(). If this parameter is 0 (zero), the mapping extends from the specified offset to the end of the file mapping.</param>
		/// <returns>Mapped view.</returns>
		/// <exception cref="System.ObjectDisposedException">The instance is disposed.</exception>
		public FileMappingView MapView(FileMappingAccess access, ulong offset, uint size)
		{
			this.VerifyNotDisposed();
			var view = new FileMappingView(this.handle, access, offset, size);
			this.views.Add(view);
			view.Disposed += this.OnViewDisposed;
			return view;
		}

		/// <summary>
		/// Maps a view of a file mapping into the address space of a calling process. A caller can optionally specify a suggested memory address for the view.
		/// </summary>
		/// <param name="access">The type of access to a file mapping object, which ensures the protection of the pages.</param>
		/// <param name="offset">The file offset where the view is to begin. The offset must specify an offset within the file mapping. It is must also match the memory allocation granularity of the system. That is, the offset must be a multiple of the allocation granularity. To obtain the memory allocation granularity of the system, use the GetSystemInfo function, which fills in the members of a SYSTEM_INFO structure.</param>
		/// <param name="size">The number of bytes of a file mapping to map to the view. All bytes must be within the maximum size specified by FileMapping.Create(). If this parameter is 0 (zero), the mapping extends from the specified offset to the end of the file mapping.</param>
		/// <param name="baseAddress">A pointer to the memory address in the calling process address space where mapping begins. This must be a multiple of the system's memory allocation granularity, or the function fails. To determine the memory allocation granularity of the system, use the GetSystemInfo function. If there is not enough address space at the specified address, the function fails. If baseAddress is IntPtr.Zero, the operating system chooses the mapping address. In this scenario, the function is equivalent to the MapViewOfFile function. While it is possible to specify an address that is safe now (not used by the operating system), there is no guarantee that the address will remain safe over time. Therefore, it is better to let the operating system choose the address. In this case, you would not store pointers in the memory mapped file, you would store offsets from the base of the file mapping so that the mapping can be used at any address.</param>
		/// <returns>Mapped view.</returns>
		/// <exception cref="System.ObjectDisposedException">The instance is disposed.</exception>
		public FileMappingView MapView(FileMappingAccess access, ulong offset, uint size, IntPtr baseAddress)
		{
			this.VerifyNotDisposed();
			var view = new FileMappingView(this.handle, access, offset, size, baseAddress);
			this.views.Add(view);
			view.Disposed += this.OnViewDisposed;
			return view;
		}

		private void OnViewDisposed(object sender, EventArgs e)
		{
			var view = (FileMappingView)sender;
			this.views.Remove(view);
			view.Disposed -= this.OnViewDisposed;
		}

		/// <summary>
		/// Creates mapping with the specified protection and size.
		/// </summary>
		/// <param name="protection">The protection.</param>
		/// <param name="maxSize">the maximum size of the file mapping object. If this parameter is 0 (zero), the maximum size of the file mapping object is equal to the current size of the file that hFile identifies. An attempt to map a file with a length of 0 (zero) fails with an error code of ERROR_FILE_INVALID. Applications should test for files with a length of 0 (zero) and reject those files.</param>
		/// <returns>Mapping object.</returns>
		public static FileMapping Create(FileMappingProtection protection, ulong maxSize)
		{
			return Create(protection, maxSize, null);
		}

		/// <summary>
		/// Creates or opens mapping with the specified protection, size and name.
		/// </summary>
		/// <param name="protection">The protection.</param>
		/// <param name="maxSize">the maximum size of the file mapping object. If this parameter is 0 (zero), the maximum size of the file mapping object is equal to the current size of the file that hFile identifies. An attempt to map a file with a length of 0 (zero) fails with an error code of ERROR_FILE_INVALID. Applications should test for files with a length of 0 (zero) and reject those files.</param>
		/// <param name="name">The name.</param>
		/// <returns>Mapping object.</returns>
		public static FileMapping Create(FileMappingProtection protection, ulong maxSize, string name)
		{
			return Create(new IntPtr(-1), protection, maxSize, name);
		}

		/// <summary>
		/// Creates mapping with the specified handle, protection and size.
		/// </summary>
		/// <param name="hFile">A handle to the file from which to create a file mapping object. The file must be opened with access rights that are compatible with the protection flags that the flProtect parameter specifies. It is not required, but it is recommended that files you intend to map be opened for exclusive access. For more information, see File Security and Access Rights. If hFile is INVALID_HANDLE_VALUE, the calling process must also specify a size for the file mapping object in the dwMaximumSizeHigh and dwMaximumSizeLow parameters. In this scenario, CreateFileMapping creates a file mapping object of a specified size that is backed by the system paging file instead of by a file in the file system.</param>
		/// <param name="protection">The protection for the file view, when the file is mapped.</param>
		/// <param name="maxSize">the maximum size of the file mapping object. If this parameter is 0 (zero), the maximum size of the file mapping object is equal to the current size of the file that hFile identifies. An attempt to map a file with a length of 0 (zero) fails with an error code of ERROR_FILE_INVALID. Applications should test for files with a length of 0 (zero) and reject those files.</param>
		/// <returns>Mapping object.</returns>
		[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "h")]
		public static FileMapping Create(IntPtr hFile, FileMappingProtection protection, ulong maxSize)
		{
			return Create(hFile, protection, maxSize, null);
		}

		/// <summary>
		/// Creates or opens mapping with the specified handle, protection, size and name.
		/// </summary>
		/// <param name="hFile">A handle to the file from which to create a file mapping object. The file must be opened with access rights that are compatible with the protection flags that the flProtect parameter specifies. It is not required, but it is recommended that files you intend to map be opened for exclusive access. For more information, see File Security and Access Rights. If hFile is INVALID_HANDLE_VALUE, the calling process must also specify a size for the file mapping object in the dwMaximumSizeHigh and dwMaximumSizeLow parameters. In this scenario, CreateFileMapping creates a file mapping object of a specified size that is backed by the system paging file instead of by a file in the file system.</param>
		/// <param name="protection">The protection for the file view, when the file is mapped.</param>
		/// <param name="maxSize">the maximum size of the file mapping object. If this parameter is 0 (zero), the maximum size of the file mapping object is equal to the current size of the file that hFile identifies. An attempt to map a file with a length of 0 (zero) fails with an error code of ERROR_FILE_INVALID. Applications should test for files with a length of 0 (zero) and reject those files.</param>
		/// <param name="name">The name of the file mapping object. If this parameter matches the name of an existing mapping object, the function requests access to the object with the protection that protection specifies. If this parameter is NULL, the file mapping object is created without a name. If lpName matches the name of an existing event, semaphore, mutex, waitable timer, or job object, the function fails.</param>
		/// <returns>Mapping object.</returns>
		[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "h")]
		public static FileMapping Create(IntPtr hFile, FileMappingProtection protection, ulong maxSize, string name)
		{
			var pHandle = NativeMethods.CreateFileMappingW(hFile, IntPtr.Zero, (uint)protection, (uint)(maxSize >> 32), (uint)(maxSize & 0xFFFFFFFF), name);
			var handle = new SafeFileHandle(pHandle, true);
			if (handle.IsInvalid) {
				var hResult = Marshal.GetHRForLastWin32Error();
				Marshal.ThrowExceptionForHR(hResult);
			}
			return new FileMapping(handle, (uint)maxSize);
		}

		/// <summary>
		/// Opens mapping with the specified name.
		/// </summary>
		/// <param name="name">The name of the file mapping object to be opened. If there is an open handle to a file mapping object by this name and the security descriptor on the mapping object does not conflict with the dwDesiredAccess parameter, the open operation succeeds.</param>
		/// <param name="access">The access to the file mapping object. This access is checked against any security descriptor on the target file mapping object.</param>
		/// <param name="inheritHandle">if set to <c>true</c>,a process created by the CreateProcess function can inherit the handle; otherwise, the handle cannot be inherited.</param>
		/// <returns>Mapping object.</returns>
		public static FileMapping Open(string name, FileMappingAccess access, bool inheritHandle)
		{
			var pHandle = NativeMethods.OpenFileMappingW((uint)access, inheritHandle, name);
			var handle = new SafeFileHandle(pHandle, true);
			if (handle.IsInvalid) {
				var hResult = Marshal.GetHRForLastWin32Error();
				Marshal.ThrowExceptionForHR(hResult);
			}
			return new FileMapping(handle, 0u);
		}

		/// <summary>
		/// Releases this instance.
		/// </summary>
		/// <returns>Handle</returns>
		/// <exception cref="System.ObjectDisposedException">The instance is disposed.</exception>
		public SafeFileHandle Release()
		{
			this.VerifyNotDisposed();
			try {
				return this.handle;
			} finally {
				this.RemoveUnmanagedDisposed(this.handle);
				this.Dispose();
			}
		}

		protected override void OnDispose()
		{
			base.OnDispose();
			this.views.Where(view => !view.IsDisposed).ToArray().ForEach(view => view.Dispose());
		}
	}
}